package ru.averta.notes.service.tag;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import ru.averta.notes.domain.TagConnector;
import ru.averta.notes.domain.ajax.AjaxTagsCombination;
import ru.averta.notes.domain.ajax.AjaxTagsSuggestAnswer;
import ru.averta.notes.domain.virtual.TagsCombination;
import ru.averta.notes.service.math.DistanceResult;
import ru.averta.notes.service.math.EditDistance;

import java.util.*;

/**
 * <code>TagSuggestorImpl</code>
 *
 * Зесь очень сложная логика... нужно все переписать =)
 *
 * @author fedor.belov (fedor.belov@mail.ru)
 *         Date: 15.09.11 14:55
 */

@Service
public class TagsSuggestServiceImpl implements TagsSuggestService {

    @Autowired private TagManager tm;
    @Autowired private EditDistance distance;

    @Override
    public List<TagsCombination> suggestTags(String prefix, List<String> sourceTags) {

        //Создаем кеш
        Map<String, Map<String, DistanceResult>> cache = new HashMap<String, Map<String, DistanceResult>>();

        //Отбираем все теги пользователя
        Collection<TagConnector> connectors = tm.findTagConnectorsByCUser();

        //Строим "карту похожести" для всех текущих тегов
        Map<String, TagDistanceMap> distanceMap = new HashMap<String, TagDistanceMap>();
        if (checkNotEmpty(prefix))
            distanceMap.put(prefix, getDistanceResultListForTag(cache, prefix, true, connectors));
        for (String tag : sourceTags)
            if (checkNotEmpty(tag)) distanceMap.put(tag, getDistanceResultListForTag(cache, tag, false, connectors));

        //Смотрим пересечения
        return getTagCombinationsFor(distanceMap);
    }

    @Override
    public List<TagsCombination> suggestTags(String prefix, String... sourceTags) {
        return suggestTags(prefix, Arrays.asList(sourceTags));
    }

    private boolean checkNotEmpty(String source) {
        return (!(source == null || source.equals("")));
    }

    private List<TagsCombination> getTagCombinationsFor(Map<String, TagDistanceMap> distanceMap) {

        List<TagsCombination> answer = new LinkedList<TagsCombination>();

        TagDistanceMap shortestMap = null;
        String shortestKey = null;

        //Получаем самую "короткую" мапу
        for (String tag : distanceMap.keySet()) {
            if (shortestMap == null || (distanceMap.get(tag).size() < shortestMap.size())) {
                shortestMap = distanceMap.get(tag);
                shortestKey = tag;
            }
        }

        //Проходимся по самой короткой мапе
        if (shortestMap != null) {
            for (Long id : shortestMap.keySet()) {

                boolean hasNotFoundTags = false;
                List<List<TagConnectorDistanceResult>> tc = new LinkedList<List<TagConnectorDistanceResult>>();

                //Ищем в остальных мапах connector'ы с таким же subject'id
                for (String tag : distanceMap.keySet()) {
                    List<TagConnectorDistanceResult> distanceResults = distanceMap.get(tag).get(id);
                    if (distanceResults == null) {
                        hasNotFoundTags = true;
                    } else {
                        tc.add(distanceResults);
                    }
                }

                if (!hasNotFoundTags) {
                    getCombinations(answer, new LinkedList<TagConnectorDistanceResult>(), tc);
                }
            }
        }

        return answer;
    }

    private void getCombinations(List<TagsCombination> answer, List<TagConnectorDistanceResult> results, List<List<TagConnectorDistanceResult>> tc) {

        if (tc.size() > 0) {
            List<TagConnectorDistanceResult> currentList = tc.get(0);
            List<List<TagConnectorDistanceResult>> nextList = new LinkedList<List<TagConnectorDistanceResult>>(tc);
            nextList.remove(0);

            for (TagConnectorDistanceResult distance : currentList) {
                List<TagConnectorDistanceResult> list = new LinkedList<TagConnectorDistanceResult>(results);
                list.add(distance);
                getCombinations(answer, list, nextList);
            }

            return;
        }


        //Дошли до последнего списка. Формируем комбинацию
        TagsCombination combination = new TagsCombination();
        for (TagConnectorDistanceResult result : results) {
            combination.addTag(result.getDistanceResult().getResultString(), result.getDistanceResult().getSameLettersBit());
        }
        addCombinationAndRecount(answer, combination);
    }

    /**
     * В ответе выдает мапу,
     * которая в ключе хранит id subject'а,
     * в значении - список похожих тегов, которые данные subject имеет
     *
     * @param tag
     * @param connectors
     * @return
     */
    private TagDistanceMap getDistanceResultListForTag(Map<String, Map<String, DistanceResult>> cache, String tag, boolean isPrefix, Collection<TagConnector> connectors) {
        //TODO оптимизировать механизм. Не строить DameauLevenshteinDistanceResult для равных слов
        //TODO прекращать считать матрицу после преодоления max значения непохожести

        TagDistanceMap distanceMap = new TagDistanceMap();

        //Проходимся по всем тегам пользователя, смотрим насколько похож каждый
        for (TagConnector tc : connectors) {
            DistanceResult distanceResult = getDamerauLevenshteinDistanceResult(cache, tag, tc.getTag().getTitle());
            if (
                    (distanceResult != null) &&
                            (isPrefix && distanceResult.isSufficientPrefixDistance()) ||
                            (!isPrefix && distanceResult.isSufficientDistance())) {
                distanceMap.add(tc.getBaseSubject().getId(), new TagConnectorDistanceResult(tc, distanceResult));
            }
        }

        return distanceMap;
    }

    private class TagDistanceMap {

        Map<Long, List<TagConnectorDistanceResult>> distanceMap = new HashMap<Long, List<TagConnectorDistanceResult>>();

        public void add(Long id, TagConnectorDistanceResult distanceResult) {
            List<TagConnectorDistanceResult> distanceList = distanceMap.get(id);
            if (distanceList == null) distanceList = new LinkedList<TagConnectorDistanceResult>();
            distanceList.add(distanceResult);
            distanceMap.put(id, distanceList);
        }

        public int size() {
            return distanceMap.size();
        }

        public Set<Long> keySet() {
            return distanceMap.keySet();
        }

        public List<TagConnectorDistanceResult> get(Long id) {
            return distanceMap.get(id);
        }

    }

    private DistanceResult getDamerauLevenshteinDistanceResult(Map<String, Map<String, DistanceResult>> cache, String source, String target) {

        DistanceResult answer;

        //Проверяем значение в кеше
        Map<String, DistanceResult> distances = cache.get(source);
        if (distances != null) {
            answer = distances.get(target);
            if (answer != null) {
                //Закешированное значение
                return answer;
            }
        } else {
            distances = new HashMap<String, DistanceResult>();
            cache.put(source, distances);
        }

        //Значение в кеше не нашли. Считаем
        answer = distance.getDamerauLevenshteinDistanceResult(source, target);

        //Кешируем
        distances.put(target, answer);

        //Возвращаем
        return answer;
    }

    private void addCombinationAndRecount(List<TagsCombination> result, TagsCombination comb) {
        for (TagsCombination combination : result) {
            if (comb.getTags().equals(combination.getTags()) &&
                    comb.getChanged().equals(combination.getChanged())) {
                combination.setCount(combination.getCount() + 1);
                return;
            }
        }

        result.add(comb);
    }

    private class TagConnectorDistanceResult {

        private DistanceResult distanceResult;
        private TagConnector tc;

        private TagConnectorDistanceResult(TagConnector tc, DistanceResult distanceResult) {
            this.distanceResult = distanceResult;
            this.tc = tc;
        }

        public DistanceResult getDistanceResult() {
            return distanceResult;
        }

        public TagConnector getTc() {
            return tc;
        }
    }
}
